/******************************************************************************
 * This program is a 100% Java Email Server.
 ******************************************************************************
 * Copyright (c) 2001-2011, Eric Daugherty (http://www.ericdaugherty.com)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the copyright holder nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 ******************************************************************************
 * For current versions and more information, please visit:
 * http://javaemailserver.sf.net/
 *
 * or contact the authors at:
 * java@ericdaugherty.com
 * andreaskyrmegalos@hotmail.com
 *
 ******************************************************************************
 * This program is based on the CSRMail project written by Calvin Smith.
 * http://crsemail.sourceforge.net/
 ******************************************************************************
 *
 * $Rev$
 * $Date$
 *
 ******************************************************************************/

package com.ericdaugherty.mail.server.info;

//Java imports
import java.io.Serializable;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Locale;

//Log imports
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

//Local imports
import com.ericdaugherty.mail.server.errors.InvalidAddressException;

/**
 * Represents a full email address, including username and domain.  This
 * class performs conversions between a full email address, and a username
 * and domain. Must be immutable.
 *
 * @author Eric Daugherty
 * @author Andreas Kyrmegalos (2.x branch)
 */
public class EmailAddress implements Serializable {

   /** Logger */
   private static Log log = LogFactory.getLog(EmailAddress.class);

   private final static Locale englishLocale = Locale.ENGLISH;

   private int hashCode;

    //***************************************************************
    // Public Interface
    //***************************************************************

    //***************************************************************
    // Constructor(s)

    /**
     * Creates an empty email address.  This is possible for
     * SMTP messages that have no MAIL FROM address.
     */
    public EmailAddress() {

       NULL = true;
       hashCode = 0;
    }

    /**
     * Creates a new instance of this class using a single string
     * that contains the full email address (e.g. joe@mydomain.com).
     */
    public EmailAddress( String fullAddress ) throws InvalidAddressException {

        int index = fullAddress.indexOf( '@' );
        if( index == -1 ) {
            throw new InvalidAddressException();
        }

        _username = parseLocalPartRFC5321(fullAddress.substring( 0, index ));
        _domain = parseDomainRFC5321(fullAddress.substring( index + 1 ));

        NULL = false;
        hashCode = 0;
        synchronized (englishLocale) {
            hashCode = 17*hashCode + _username.toLowerCase(englishLocale).hashCode();
            hashCode = 17*hashCode + _domain.toLowerCase(englishLocale).hashCode();
        }
    }

    /**
     * Creates a new instance of this class using a username string
     * and an address string.
     */
    public EmailAddress( String username, String domain ) throws InvalidAddressException {
       this(username+'@'+domain);
    }

    /**
     * This is used to suppress the thrown exception when username and domain
     * have already been verified
     * @param username the local-part
     * @param domain the domain or address-literal
     * @return an instance of this class
     */
    public static EmailAddress getEmailAddress(String username, String domain) {
      try {
         return new EmailAddress(username, domain);
      } catch (InvalidAddressException ex) {
         //
         return null;
      }
    }

    /**
     * Override toString to return the full address
     */
    @Override
    public String toString() {
        return getAddress();
    }

    //***************************************************************
    //JavaBean Methods
    
    public boolean isNULL() {
       return NULL;
    }

    public boolean isMailerDaemon() {
        synchronized (englishLocale) {
           return _username.toUpperCase(englishLocale).equals("MAILER_DAEMON");
        }
    }

    public String getUsername(){
        if( NULL ) {
            return "";
        }
        else {
            return _username;
        }
    }

    public final String parseLocalPartRFC5321(String localPart) throws InvalidAddressException{

       if (localPart.length()==0) {
          throw new InvalidAddressException("Zero length local-part");
       }
       //Quoted-string
       if (localPart.charAt(0)=='"') {
          if (localPart.charAt(localPart.length()-1)!='"'||
                (localPart.length()>=4&&localPart.charAt(localPart.length()-2)=='\\'&&
                 localPart.charAt(localPart.length()-3)!='\\')) {
             throw new InvalidAddressException("Malformed local-part Quoted-string");
          }
          if (localPart.length()==3&&localPart.charAt(1)=='\\') {
             throw new InvalidAddressException("Illegal local-part quoted-pairSMTP character "+localPart.charAt(1));
          }
          int length = localPart.length()-1;
          //quoted-pairSMTP
          if (localPart.charAt(1)=='\\') {
             for (int i=2;i<length;i++) {
                if (i%2!=0) {
                   if (localPart.charAt(i)!='\\') {
                      throw new InvalidAddressException("Malformed local-part quoted-pairSMTP (expected backslash)");
                   }
                }
                else {
                   if (localPart.charAt(i)<32||localPart.charAt(i)>126) {
                      throw new InvalidAddressException("Illegal local-part quoted-pairSMTP character "+localPart.charAt(i));
                   }
                }
             }
          }
          //qtextSMTP
          else {
             for (int i=1;i<length;i++) {
                if (localPart.charAt(i)<32||localPart.charAt(i)>126||
                      localPart.charAt(i)==34||localPart.charAt(i)==92) {
                      throw new InvalidAddressException("Illegal local-part quoted-pairSMTP character "+localPart.charAt(i));
                   }
             }
          }
       }
       //Dot-string
       else {
          String check;
          synchronized (englishLocale) {
             check = localPart.toLowerCase(englishLocale);
          }
          int length = check.length();
          if (check.charAt(0)=='.') {
             throw new InvalidAddressException("Illegal local-part Dot-string character (first character can not be a dot)");
          }
          if (!charInAtextLowerCaseOnly(check.charAt(0))) {
             throw new InvalidAddressException("Illegal local-part Dot-string character (character "+localPart.charAt(0)+" not an atext)");
          }
          for (int i=1;i<length;i++) {
             if (!charInAtextLowerCaseOnly(check.charAt(i)) && check.charAt(i)!='.') {
                throw new InvalidAddressException("Illegal local-part Dot-string character (character "+localPart.charAt(i)+" not an atext or a .)");
             }
          }
       }

       return localPart;
    }
    
    private boolean charInAtextLowerCaseOnly(char aChar) {
       
       for (int i=0;i<55;i++) {
          if (aChar==ATEXT_LOWERCASE_ONLY[i]) return true;
       }
       return false;
    }

    private static final char[] ATEXT_LOWERCASE_ONLY = new char[] {
       '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/',
       '0', '1', '2', '3', '4', '5' , '6', '7', '8', '9',
       '=', '?',
       'a', 'b', 'c', 'd', 'e', 'f' , 'g', 'h', 'i', 'j',
       'k', 'l', 'm', 'n', 'o', 'p' , 'q', 'r', 's', 't',
       'u', 'v', 'w', 'x', 'y', 'z' ,
       '^', '_', '`',
       '{', '|', '}','~'
    };

    public String getDomain(){
        if( NULL ) {
            return "";
        }
        else {
            return _domain;
        }
    }

    public final String parseDomainRFC5321(String domain) throws InvalidAddressException{

       //address-literal (it is explicitly assumed that if an address-literal is used
       //that such an address-literal will not fail a DNS lookup. Since use of address-
       //literal is somewhat limited this doesn't place considerable burden upon a
       //nameservice)
       if (domain.charAt(0)=='[') {
          if (domain.charAt(domain.length()-1)!=']') {
             throw new InvalidAddressException(domain+" starts with a bracket but doesn't finish with one.");
          }
          String check = domain;
          if (check.indexOf('.')!=-1) {
             //Get rid of the brackets, otherwise will be mistaken for a IPv6
             check = domain.substring(1, domain.length()-1);
          }
          try {
             InetAddress.getByName(check);
          }
          catch (UnknownHostException uhe) {
             throw new InvalidAddressException(domain+" is not a valid address-literal.");
          }
          //Get rid of the brackets anyway
          check = domain.substring(1, domain.length()-1);
       }
       //domain (unlike in the address-literal case use of the InetAddress facility
       //is avoided and the domain is parsed as simple text)
       else {
          String check;
          synchronized (englishLocale) {
             check = domain.toLowerCase(englishLocale);
          }
          if (check.length()==0) {
             throw new InvalidAddressException("Domain can not be zero length.");
          }
          else if (check.length()>255) {
             throw new InvalidAddressException("Domain can not have length greater than 255 octets.");
          }
          String[] parts = check.split("\\.");
          if (parts.length==0) {
             throw new InvalidAddressException("Domain not acceptable. There must be at least one domain component(label).");
          }
          else if (parts.length==1) {
             if (!parts[0].equals("localhost")) {
                throw new InvalidAddressException("Top-level domains are not acceptable.");
             }else {
                log.warn("The localhost is used as a mail domain.");
             }
          }
          for (int i=0;i<parts.length;i++) {
             if (parts[i].length()<=1) {
                throw new InvalidAddressException("a sub domain must be at least two characters long");
             }
             else if (parts[i].length()>63) {
                throw new InvalidAddressException("a sub domain must be at most sixty three characters long");
             }
             if (parts[i].charAt(0)=='-'||parts[i].charAt(parts[i].length()-1)=='-') {
                throw new InvalidAddressException("The first or last character of a sub-domain can not be a hyphen.");
             }
             for (int j=0;j<parts[i].length();j++) {
                if (!charInAlphaDigitHyphenLowerCaseOnly(parts[i].charAt(j))) {
                   throw new InvalidAddressException("Illegal sub-domain ALPHA/DIGIT/- character ' "+parts[i].charAt(j)+" '.");
                }
             }
          }
       }
       return domain;

    }
    
    private boolean charInAlphaDigitHyphenLowerCaseOnly(char aChar) {
       
       for (int i=0;i<37;i++) {
          if (aChar==ALPHA_DIGIT_HYPHEN_LOWERCASE_ONLY[i]) return true;
       }
       return false;
    }

    private static final char[] ALPHA_DIGIT_HYPHEN_LOWERCASE_ONLY = new char[]{
       '0', '1', '2', '3', '4', '5' , '6', '7', '8', '9',
       'a', 'b', 'c', 'd', 'e', 'f' , 'g', 'h', 'i', 'j',
       'k', 'l', 'm', 'n', 'o', 'p' , 'q', 'r', 's', 't',
       'u', 'v', 'w', 'x', 'y', 'z' , '-'
    };

    public String getAddress() {
        return getFullAddress( getUsername(), getDomain() );
    }

    //***************************************************************
    // Private Interface
    //***************************************************************

    /**
     * Combines a username and domain into a single email address.
     */
    private String getFullAddress( String username, String domain ) {

        if( NULL ) {
            return "";
        }
        else {
            StringBuilder fullAddress = new StringBuilder( username.length()+1+domain.length() );
            fullAddress.append(username).append( '@' ).append( domain );

            return fullAddress.toString();
        }
    }

    //***************************************************************
    // Variables
    //***************************************************************

    private String _username = "";
    private String _domain = "";
    private boolean NULL;

    @Override
    public int hashCode() {
       return hashCode;
    }

    @Override
    public boolean equals(Object object) {
       if (!(object instanceof EmailAddress)) {
         return false;
      }
      EmailAddress that = (EmailAddress) object;
      if ((this._username == null && that._username != null) || (this._username != null && !this._username.equalsIgnoreCase(that._username))) {
         return false;
      }
      if ((this._domain == null && that._domain != null) || (this._domain != null && !this._domain.equalsIgnoreCase(that._domain))) {
         return false;
      }
      return true;
    }
}
//EOF